/*
 * Copyright (c) 2004-2006, Volatile-Engine All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Volatile-Engine nor the
 * names of its contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */
package com.volatileengine.image.loader;

import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;

import com.volatileengine.datatypes.ArrayByte;
import com.volatileengine.image.DXT1AImage;
import com.volatileengine.image.DXT1Image;
import com.volatileengine.image.DXT3Image;
import com.volatileengine.image.DXT5Image;
import com.volatileengine.image.Mipmap;
import com.volatileengine.image.Texture;
import com.volatileengine.util.LittleEndianDataInputStream;

/**
 * @author ChaosDeathFish, darkprophet
 * 
 */
public class DDSReader {

	private enum ImageType {
		DXT1, DXT1A, DXT3, DXT5;
	}

	private static final int DDSD_MANDATORY = 0x1007;
	private static final int DDSD_MIPMAPCOUNT = 0x20000;
	private static final int DDSD_LINEARSIZE = 0x80000;
	private static final int DDSD_DEPTH = 0x800000;

	private static final int DDPF_ALPHAPIXELS = 0x1;
	private static final int DDPF_FOURCC = 0x4;
	// private static final int DDPF_RGB = 0x40;

	private static final int DDSCAPS_COMPLEX = 0x8;
	private static final int DDSCAPS_TEXTURE = 0x1000;
	private static final int DDSCAPS_MIPMAP = 0x400000;

	private static final int DDSCAPS2_CUBEMAP = 0x200;
	private static final int DDSCAPS2_VOLUME = 0x200000;

	private static final int PF_DXT1 = 0x31545844;
	private static final int PF_DXT3 = 0x33545844;
	private static final int PF_DXT5 = 0x35545844;

	private int width;
	private int height;
	private int flags;
	private int pitchOrSize;
	private int mipMapCount;

	private boolean compressed;
	private ImageType pixelFormat;
	private int bpp;
	private int[] sizes;

	private DataInput input;

	public DDSReader(InputStream in) {
		input = new LittleEndianDataInputStream(in);
	}

	public Texture createImage(String texname) throws IOException {
		// load the header and read data
		loadHeader();
		ByteBuffer data = readData();

		Texture tex = new Texture(texname, sizes.length);

		Mipmap[] images = new Mipmap[sizes.length];

		int mipmapWidth = width;
		int mipmapHeight = height;

		for (int i = 0; i < sizes.length; i++) {
			ArrayByte dataarray = new ArrayByte(1);
			dataarray.setLength(sizes[i]);
			if (i != 0) {
				data.position(sizes[i - 1]);
			}
			data.limit(sizes[i]);
			dataarray.getBuffer().put(data);
			dataarray.getBuffer().rewind();

			images[i] = createImage(tex, i, mipmapWidth, mipmapHeight, dataarray);

			mipmapWidth /= 2;
			mipmapHeight /= 2;
		}

		return tex;

	}

	private Mipmap createImage(Texture tex, int level, int width, int height, ArrayByte dataarray) {
		switch (pixelFormat) {
		case DXT1:
			return new DXT1Image(tex, level, width, height, dataarray);
		case DXT1A:
			return new DXT1AImage(tex, level, width, height, dataarray);
		case DXT3:
			return new DXT3Image(tex, level, width, height, dataarray);
		case DXT5:
			return new DXT5Image(tex, level, width, height, dataarray);
		}
		return null;
	}

	private void loadHeader() throws IOException {
		if (input.readInt() != 0x20534444 || input.readInt() != 124) {
			throw new IOException("Not a DDS file");
		}

		flags = input.readInt();

		if (!is(flags, DDSD_MANDATORY)) {
			throw new IOException("Mandatory flags missing");
		}
		if (is(flags, DDSD_DEPTH)) {
			throw new IOException("Depth not supported");
		}

		height = input.readInt();
		width = input.readInt();
		pitchOrSize = input.readInt();
		input.skipBytes(4);
		mipMapCount = input.readInt();
		input.skipBytes(44);
		readPixelFormat();
		int caps1 = input.readInt();
		int caps2 = input.readInt();
		input.skipBytes(12);

		if (!is(caps1, DDSCAPS_TEXTURE)) {
			throw new IOException("File is not a texture");
		}
		if (is(caps2, DDSCAPS2_CUBEMAP)) {
			throw new IOException("Cubemaps not supported");
		}
		if (is(caps2, DDSCAPS2_VOLUME)) {
			throw new IOException("Volume textures not supported");
		}

		int expectedMipmaps = 1 + (int) Math.ceil(Math.log(Math.max(height, width)) / Math.log(2));

		if (is(caps1, DDSCAPS_MIPMAP)) {
			if (!is(caps1, DDSCAPS_COMPLEX)) {
				throw new IOException("File contains mipmaps but does not have the Complex DWord");
			}
			if (!is(flags, DDSD_MIPMAPCOUNT)) {
				mipMapCount = expectedMipmaps;
			} else if (mipMapCount != expectedMipmaps) {
				throw new IOException("Got " + mipMapCount + "mipmaps, expected" + expectedMipmaps);
			}
		} else {
			mipMapCount = 1;
		}

		loadSizes();
	}

	private void readPixelFormat() throws IOException {
		int pfSize = input.readInt();
		if (pfSize != 32) {
			throw new IOException("Pixel format size is " + pfSize + ", not 32");
		}

		int dxtFlags = input.readInt();

		if (is(dxtFlags, DDPF_FOURCC)) {
			if (!is(flags, DDSD_LINEARSIZE)) {
				throw new IOException("Must use linear size with fourcc");
			}

			compressed = true;
			int fourcc = input.readInt();
			input.skipBytes(20);

			switch (fourcc) {
			case PF_DXT1:
				bpp = 4;
				if (is(dxtFlags, DDPF_ALPHAPIXELS)) {
					pixelFormat = ImageType.DXT1A;
				} else {
					pixelFormat = ImageType.DXT1;
				}
				break;
			case PF_DXT3:
				bpp = 8;
				pixelFormat = ImageType.DXT3;
				break;
			case PF_DXT5:
				bpp = 8;
				pixelFormat = ImageType.DXT5;
				break;
			default:
				throw new IOException("Unknown fourcc: " + string(fourcc));
			}

			int size = ((width + 3) / 4) * ((height + 3) / 4) * bpp * 2;
			if (pitchOrSize != size) {
				throw new IOException("Expected size = " + size + ", real = " + pitchOrSize);
			}
		} else {
			compressed = false;
			throw new IOException("Uncompressed not supported");
		}
	}

	private void loadSizes() {
		int w = width;
		int h = height;

		sizes = new int[mipMapCount];

		for (int i = 0; i < mipMapCount; i++) {
			int size;

			if (compressed) {
				size = ((w + 3) / 4) * ((h + 3) / 4) * bpp * 2;
			} else {
				throw new RuntimeException("Uncompressed not supported");
			}

			sizes[i] = ((size + 3) / 4) * 4;

			w = Math.max(w / 2, 1);
			h = Math.max(h / 2, 1);
		}
	}

	private ByteBuffer readData() throws IOException {
		int totalSize = 0;

		for (int i = 0; i < sizes.length; i++) {
			totalSize += sizes[i];
		}

		byte[] data = new byte[totalSize];
		input.readFully(data);

		ByteBuffer buffer = ByteBuffer.allocateDirect(totalSize);
		buffer.put(data);
		buffer.rewind();

		return buffer;
	}

	private boolean is(int flags, int mask) {
		return (flags & mask) == mask;
	}

	private String string(int value) {
		StringBuffer buf = new StringBuffer();

		buf.append((char) (value & 0xFF));
		buf.append((char) ((value & 0xFF00) >> 8));
		buf.append((char) ((value & 0xFF0000) >> 16));
		buf.append((char) ((value & 0xFF00000) >> 24));

		return buf.toString();
	}
}
